/**
 * This file is part of Sonedyan.
 * 
 * Sonedyan is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation;
 * either version 3 of the License, or (at your option) any
 * later version.
 *
 * Sonedyan is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public
 * License along with Octave; see the file COPYING.  If not
 * see <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2009-2012 Jimmy Dubuisson <jimmy.dubuisson@gmail.com>
 */

package org.unige.mpej.eckmann.sonedyan.enron.db;

import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import com.db4o.Db4oEmbedded;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.EmbeddedConfiguration;
import com.db4o.config.QueryEvaluationMode;

import com.db4o.io.CachingStorage;
import com.db4o.io.FileStorage;
import com.db4o.io.NonFlushingStorage;
import com.db4o.io.Storage;
import com.db4o.query.Constraint;
import com.db4o.query.Query;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.internet.InternetAddress;

import org.apache.log4j.Logger;
import org.unige.mpej.eckmann.sonedyan.enron.bean.Actor;
import org.unige.mpej.eckmann.sonedyan.enron.bean.Email;
import org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage;
import org.unige.mpej.eckmann.sonedyan.enron.bean.Link;

/**
 * DB4o database handler
 */
public class Db4oHandler extends DatabaseHandler
{
	private static Logger log = Logger.getLogger(org.unige.mpej.eckmann.sonedyan.enron.db.Db4oHandler.class);
	
	private static final String DB_FILE_NAME = "db4o/data";
	// enable support for large databses
	private static final int DB_BLOCK_SIZE = 8;
	private static final boolean DB_FLUSH_DISABLED = false;
	private static final int DEFAULT_ACTIVATION_DEPTH = 1;
	
	// DB container
	private ObjectContainer container;
	// DB configuration
	private EmbeddedConfiguration config;
	
	private int commitCounter = 0;
	
	/**
	 * class constructor
	 */
	public Db4oHandler()
	{
		this(DB_FILE_NAME);
	}
	
	/**
	 * class constructor
	 */
	public Db4oHandler(String fileName)
	{
		try
		{
			log.debug("Opening db4o database file '" + fileName + "'");
			
			this.config = Db4oEmbedded.newConfiguration();
			
			// configure the database
			this.configureDatabase();
			
			this.container = Db4oEmbedded.openFile(this.config, fileName);
		}
		catch(Exception e)
		{
			log.error("Container could not be initialized: " + e.getMessage());
		}
	}
	
	/** 
	 * configure the database
	 */
	public void configureDatabase()
	{
		// set support for large databases
		this.config.file().blockSize(DB_BLOCK_SIZE);
		
		// set the query mode to LAZY for increased performance (other possibilities include SNAPSHOT and IMMEDIATE)
		this.config.common().queries().evaluationMode(QueryEvaluationMode.LAZY);
		
		if (DB_FLUSH_DISABLED)
		{
			// make commits very efficient but increase the risk of DB corruption
			Storage nonFlushingStorage = new NonFlushingStorage(new CachingStorage(new FileStorage()));
			this.config.file().storage(nonFlushingStorage);
		}
		
		// set the activation depth
		this.config.common().activationDepth(DEFAULT_ACTIVATION_DEPTH);
		
		// index fields
		// this.setIndexes(this.config);
	}
	
	/**
	 * set indexes
	 * 
	 * NB: use db4o annotations instead
	 */
	@Deprecated
	public void setIndexes(EmbeddedConfiguration config)
	{
		// index the EnronMessage instances fields
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("id").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("relativeFilePath").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("sentDate").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("subject").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("from").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("toRecipients").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("ccRecipients").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("bccRecipients").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("numberOfToRecipients").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("numberOfCcRecipients").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.EnronMessage.class).objectField("numberOfBccRecipients").indexed(true);
	
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("id").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("firstName").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("lastName").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("sex").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("hierarchyLevel").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("jobDescription").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("isKown").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Actor.class).objectField("emails").indexed(true);
		
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Email.class).objectField("address").indexed(true);
		this.config.common().objectClass(org.unige.mpej.eckmann.sonedyan.enron.bean.Email.class).objectField("type").indexed(true);
	}
	
	public ObjectContainer getContainer()
	{
		return container;
	}

	public void setContainer(ObjectContainer container)
	{
		this.container = container;
	}

	public EmbeddedConfiguration getConfig()
	{
		return config;
	}

	public void setConfig(EmbeddedConfiguration config)
	{
		this.config = config;
	}

	/**
	 * store the specified object
	 */
	public void store(Object o)
	{
		try
		{
			this.container.store(o);
		}
		catch(Exception e)
		{
			log.error("Object could not be stored in the database: " + e.getMessage());
		}
	}
	
	/**
	 * commit operation
	 */
	public void commit()
	{
		try
		{
			this.container.commit();
		}
		catch(Exception e)
		{
			log.error("Operation could not be commited: " + e.getMessage());
		}
	}
	
	/**
	 * close the database connection 
	 */
	public boolean close()
	{
		if (this.container != null)
		{
			return this.container.close();
		}
		else
		{
			log.debug("Container was not initialized");
			return true;
		}
	}
	
	/**
	 * store email
	 */
	public int storeEmail(Email email)
	{
		this.store(email);
		return 0;
	}
	
	/**
	 * store link
	 */
	public int storeLink(Link link)
	{
		this.store(link);
		return 0;
	}
	
	/**
	 * store actor
	 */
	public int storeActor(Actor actor)
	{
		this.store(actor);
		this.commit();
		return 0;
	}
	
	/**
	 * store message
	 */
	public int storeMessage(EnronMessage message)
	{
		this.store(message);
		this.commitCounter++;
		
		// NB: commiting at regular intervals prevents the database from freezing after ~100'000 adds
		if (this.commitCounter == 10000)
		{
			this.commit();
			this.commitCounter = 0;
		}
		
		return 0;
	}
	
	/**
	 * get the set of objects of the specified class
	 */
	public <T> List<T> getInstancesByClass(Class<T> c)
	{
		return this.container.queryByExample(c);
	}
	
	/**
	 * get message specified by its ID 
	 */
	public EnronMessage getMessageById(String id)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			query.descend("id").constrain(id);
			
			ObjectSet<EnronMessage> result = query.execute();
			
			if (result.size() == 1)
			{
				return result.get(0);
			}
			else
			{
				log.info("Message '" + id + "' could not be retrieved");
				return null;
			}
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get messages with a unique sender and a unique recipient (0 CC and 0 BCC)
	 */
	public List<EnronMessage> getUniqueRecipientMessages()
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			
			Query query1 = query.descend("numberOfToRecipients");
			Query query2 = query.descend("numberOfCcRecipients");
			Query query3 = query.descend("numberOfBccRecipients");

			query1.constrain(1).and(query2.constrain(0)).and(query3.constrain(0));
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get messages with a unique sender and a unique recipient in a date range
	 */
	public List<EnronMessage> getUniqueRecipientMessagesInDateRange(Date beginDate, Date endDate)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			
			Query dateQuery = query.descend("sentDate");
			Query toQuery = query.descend("numberOfToRecipients");
			Query ccQuery = query.descend("numberOfCcRecipients");
			Query bccQuery = query.descend("numberOfBccRecipients");
			
			Constraint constraint1 = dateQuery.constrain(beginDate).greater();
			Constraint constraint2 = dateQuery.constrain(endDate).smaller();
			
			toQuery.constrain(1).and(ccQuery.constrain(0)).and(bccQuery.constrain(0)).and(constraint1).and(constraint2);
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get links in a date range
	 */
	public List<Link> getLinksInDateRange(Date beginDate, Date endDate)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(Link.class);
			query.constrain(beginDate).greater();
			query.constrain(endDate).smaller();
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get the specified Enron actor
	 */
	public Actor getActorByAddress(String address)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(Actor.class);
			query.descend("emails").descend("address").descend("address").constrain(address).contains();
			
			ObjectSet<Actor> results = query.execute();
			
			if (results.size() != 0)
			{
				return results.next();
			}
			else
			{
				log.debug("The specified actor '" + address + "' was not found...");
				
				return null;
			}
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get the specified Enron actor
	 */
	public Actor getActorById(String id)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(Actor.class);
			query.descend("id").constrain(id);
			
			ObjectSet<Actor> results = query.execute();
			
			if (results.size() != 0)
			{
				return results.next();
			}
			else
			{
				log.debug("The specified actor '" + id + "' was not found...");
				
				return null;
			}
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get the list of all emails
	 * 
	 * NB: Does not set the email type!
	 */
	public Hashtable<Integer, Address> getAllEmails()
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			
			Query query1 = query.descend("from").descend("address");
			Query query2 = query.descend("toRecipients").descend("address");
			Query query3 = query.descend("ccRecipients").descend("address");
			Query query4 = query.descend("bccRecipients").descend("address");
			
			List<String> list1 = query1.execute();
			List<String> list2 = query2.execute();
			List<String> list3 = query3.execute();
			List<String> list4 = query4.execute();
			
			Hashtable<Integer, Address> emails = new Hashtable<Integer, Address>();
			
			for (String emailString : list1)
			{
				Address address = new InternetAddress(emailString);
				int hash = emailString.hashCode();
				if (!emails.contains(hash)){ emails.put(emailString.hashCode(), address); }
			}
			
			for (String emailString : list2)
			{
				Address address = new InternetAddress(emailString);
				int hash = emailString.hashCode();
				if (!emails.contains(hash)){ emails.put(emailString.hashCode(), address); }
			}
			
			for (String emailString : list3)
			{
				Address address = new InternetAddress(emailString);
				int hash = emailString.hashCode();
				if (!emails.contains(hash)){ emails.put(emailString.hashCode(), address); }
			}
			
			for (String emailString : list4)
			{
				Address address = new InternetAddress(emailString);
				int hash = emailString.hashCode();
				if (!emails.contains(hash)){ emails.put(emailString.hashCode(), address); }
			}
			
			return emails;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get the set of all messages
	 * 
	 * NB: loading the whole set of messages is slow!
	 */
	@Deprecated
	public List<EnronMessage> getAllMessages()
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * store all the links
	 */
	public boolean storeAllLinks()
	{
		try
		{
			List<EnronMessage> messages = this.getAllMessages();
			Hashtable<String, Actor> actors = new Hashtable<String, Actor>();
			
			for (EnronMessage message : messages)
			{
				String fromAddress = EnronMessage.getAddressAsString(message.getFrom());
				Address[] toRecipients = message.getRecipients(Message.RecipientType.TO);
				Address[] ccRecipients = message.getRecipients(Message.RecipientType.CC);
				Address[] bccRecipients = message.getRecipients(Message.RecipientType.BCC);
				
				// TO recipients
				if (toRecipients != null && toRecipients.length > 0)
				{
					for (int i = 0; i < toRecipients.length; i++)
					{
						String toAddress = toRecipients[i].toString();
						
						if (Email.isEnronEmail(fromAddress) && Email.isEnronEmail(toAddress))
						{
							Actor fromActor = actors.get(fromAddress);
							
							if (fromActor == null)
							{
								fromActor = this.getActorByAddress(fromAddress);
							}
							
							Actor toActor = actors.get(toAddress);
							
							if (toActor == null)
							{
								toActor = this.getActorByAddress(toAddress);
							}
							
							if (fromActor != null && toActor != null)
							{
								actors.put(fromAddress, fromActor);
								actors.put(toAddress, toActor);
								
								if (!fromActor.getId().equals(toActor.getId()))
								{
									log.info("Storing 'TO' link from actor '" + fromActor.getId() + "' to actor '" + toActor.getId() + "'");
									
									Link link = new Link();
									link.setFromActorId(fromActor.getId());
									
									// remove '<' & '>' characters from messageId
									// <messageId> -> messageId
									link.setLinkId(message.getId().replace("<", "").replace(">", ""));
									
									link.setStartDate(message.getSentDate());
									link.setType(Message.RecipientType.TO);
									link.setToActorId(toActor.getId());
									
									this.storeLink(link);
								}
							}
						}
					}
				} // TO
				
				// CC recipients
				if (ccRecipients != null && ccRecipients.length > 0)
				{
					for (int i = 0; i < ccRecipients.length; i++)
					{
						String toAddress = ccRecipients[i].toString();
						
						if (Email.isEnronEmail(fromAddress) && Email.isEnronEmail(toAddress))
						{
							Actor fromActor = actors.get(fromAddress);
							
							if (fromActor == null)
							{
								fromActor = this.getActorByAddress(fromAddress);
							}
							
							Actor toActor = actors.get(toAddress);
							
							if (toActor == null)
							{
								toActor = this.getActorByAddress(toAddress);
							}
							
							if (fromActor != null && toActor != null)
							{
								actors.put(fromAddress, fromActor);
								actors.put(toAddress, toActor);
								
								if (!fromActor.getId().equals(toActor.getId()))
								{
									log.info("Storing 'CC' link from actor '" + fromActor.getId() + "' to actor '" + toActor.getId() + "'");
									
									Link link = new Link();
									link.setFromActorId(fromActor.getId());
									
									// remove '<' & '>' characters from messageId
									// <messageId> -> messageId
									link.setLinkId(message.getId().replace("<", "").replace(">", ""));
									
									link.setStartDate(message.getSentDate());
									link.setType(Message.RecipientType.CC);
									link.setToActorId(toActor.getId());
									
									this.storeLink(link);
								}
							}
						}
					} 
				} // CC
				
				// BCC recipients
				if (bccRecipients != null && bccRecipients.length > 0)
				{
					for (int i = 0; i < bccRecipients.length; i++)
					{
						String toAddress = bccRecipients[i].toString();
						
						if (Email.isEnronEmail(fromAddress) && Email.isEnronEmail(toAddress))
						{
							Actor fromActor = actors.get(fromAddress);
							
							if (fromActor == null)
							{
								fromActor = this.getActorByAddress(fromAddress);
							}
							
							Actor toActor = actors.get(toAddress);
							
							if (toActor == null)
							{
								toActor = this.getActorByAddress(toAddress);
							}
							
							if (fromActor != null && toActor != null)
							{
								actors.put(fromAddress, fromActor);
								actors.put(toAddress, toActor);
								
								if (!fromActor.getId().equals(toActor.getId()))
								{
									log.info("Storing 'BCC' link from actor '" + fromActor.getId() + "' to actor '" + toActor.getId() + "'");
									
									Link link = new Link();
									link.setFromActorId(fromActor.getId());
									
									// remove '<' & '>' characters from messageId
									// <messageId> -> messageId
									link.setLinkId(message.getId().replace("<", "").replace(">", ""));
									
									link.setStartDate(message.getSentDate());
									link.setType(Message.RecipientType.BCC);
									link.setToActorId(toActor.getId());
									
									this.storeLink(link);
								}
							}
						}
					}
				} // BCC
			} // for
			
			return true;
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return false;
		}
	}
	
	/**
	 * get the set of messages according to the specified from address
	 */
	public List<EnronMessage> getMessagesByFrom(String fromAddress)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			query.descend("from").descend("address").constrain(fromAddress);
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
	/**
	 * get the set of messages according to the specified to address
	 */
	public List<EnronMessage> getMessagesByTo(String toAddress)
	{
		try
		{
			Query query = this.container.query();
			
			query.constrain(EnronMessage.class);
			query.descend("toRecipients").descend("address").constrain(toAddress);
			
			return query.execute();
		}
		catch(Exception e)
		{
			log.error("An error occured: " + e.getMessage());
			return null;
		}
	}
	
}
